package com.vaadin.thomas.contenttool.data;

import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;

import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.Transaction;

import com.vaadin.thomas.contenttool.exceptions.ContentModelException;
import com.vaadin.thomas.contenttool.exceptions.ValueNotFoundException;

public class Version extends AbstractData implements Comparable<Version> {

	public static final String PROP_VERSION = "version";
	public static final String PROP_COMMITTED = "date";
	public static final String PROP_COMMITTER = "committer";
	public static final String PROP_MESSAGE = "msg";

	public static final int VERSION_PREVIEW = Integer.MAX_VALUE;

	private int version = -1;

	Version(Node v) {
		super(v);
	}

	// TODO optimize
	public Value getValue(String identifier) {

		Node v = null;
		for (Relationship r : node
				.getRelationships(References.VERSION_TO_VALUES)) {
			if (identifier.equals(r.getEndNode().getProperty(Value.PROP_ID))) {
				v = r.getEndNode();
				break;
			}
		}

		if (v == null && getPreviousVersion() != null) {
			return getPreviousVersion().getValue(identifier);
		}

		if (v == null) {
			return null;
		}
		return new Value(v);
	}

	// TODO optimize
	public Value getValueInThisOrCreate(String identifier) {

		Node v = null;
		for (Relationship r : node
				.getRelationships(References.VERSION_TO_VALUES)) {
			if (identifier.equals(r.getEndNode().getProperty(Value.PROP_ID))) {
				v = r.getEndNode();
				break;
			}
		}

		if (v == null) {
			Model.get();
			Transaction tx = Model.db.beginTx();
			try {
				Value val = Value.create(identifier);
				addValue(val);
				v = val.node;
				tx.success();
			} catch (RuntimeException e) {
				tx.failure();
			} finally {
				tx.finish();
			}
		}

		return new Value(v);
	}

	public Map<String, Value> getValuesInThis() {

		Map<String, Value> m = new HashMap<String, Value>();
		for (Relationship r : node.getRelationships(Direction.OUTGOING,
				References.VERSION_TO_VALUES)) {
			Value v = new Value(r.getEndNode());
			m.put(v.getId(), v);
		}
		return m;
	}

	public Map<String, Value> getAllValues() {
		Map<String, Value> m = new HashMap<String, Value>();

		if (getPreviousVersion() != null) {
			m.putAll(getPreviousVersion().getAllValues());
		}

		for (Relationship r : node.getRelationships(Direction.OUTGOING,
				References.VERSION_TO_VALUES)) {
			Value v = new Value(r.getEndNode());

			m.put(v.getId(), v);
		}
		return m;
	}

	/**
	 * Adds the new value to this {@link Version}, if this version is Preview (
	 * {@link #isPreview()}) and a {@link Value} with the given identifier
	 * doesn't exist already.
	 * <p>
	 * Must be called while inside a {@link Transaction}.
	 * 
	 * @param value
	 * @throws ContentModelException
	 *             if this {@link Version} is not the Preview version, or if a
	 *             {@link Value} with the given identifier already exists in
	 *             this Version.
	 */
	void addValue(Value value) {

		if (getValuesInThis().get(value.getId()) != null) {
			throw new ContentModelException("already exists in this version");
		}
		if (!isPreview()) {
			throw new ContentModelException(
					"new values are only allowed in preview");
		}

		// add to this
		node.createRelationshipTo(value.node, References.VERSION_TO_VALUES);

		// make sure node is connected to history
		Version previousVersion = getPreviousVersion();
		Value previousVal = null;
		while (previousVal == null && previousVersion != null) {
			previousVal = previousVersion.getValue(value.getId());
			previousVersion = previousVersion.getPreviousVersion();
		}

		if (previousVal != null) {
			previousVal.node.createRelationshipTo(value.node,
					References.VALUE_TO_NEWER);
			// copy tags
			for (Tag t : previousVal.getTags()) {
				t.node.createRelationshipTo(value.node,
						References.TAG_TO_VALUES);
			}
		}

	}

	public boolean isPreview() {
		return VERSION_PREVIEW == getVersionNumber();
	}

	/**
	 * Must be called inside a {@link Transaction}.
	 */
	void setVersionProperty(String key, Object value) {
		node.setProperty(key, value);
	}

	public Map<String, Object> getVersionProperties() {

		Map<String, Object> m = new HashMap<String, Object>();
		for (String s : node.getPropertyKeys()) {
			m.put(s, node.getProperty(s));
		}

		return m;
	}

	/**
	 * Must be called inside a {@link Transaction}.
	 */
	void setVersionProperties(Map<String, String> map) {
		for (Entry<String, String> e : map.entrySet()) {
			setVersionProperty(e.getKey(), e.getValue());
		}
	}

	@Override
	public int compareTo(Version other) {
		return getVersionNumber() - other.getVersionNumber();
	}

	public int getVersionNumber() {
		if (version == -1) {
			version = NodeUtil.getInt(node, PROP_VERSION);
		}
		return version;
	}

	public Date getCommitDate() {
		return new Date((Long) node.getProperty(PROP_COMMITTED));
	}

	public String getCommitterId() {
		return node.getProperty(PROP_COMMITTER).toString();
	}

	/**
	 * Get the correct String value to display when running this {@link Version}
	 * of the data. Uses {@link Version} history if needed.
	 * <p>
	 * If the identifier doesn't match any data in the system, the identifier
	 * itself is returned.
	 */
	public String get(String identifier, Locale loc) {

		// System.out.println("Fetching " + identifier + " from version "
		// + getVersionNumber());

		Value val = getValue(identifier);
		if (val == null) {
			System.err.println("Value not found: " + identifier);
			return identifier;
		}

		String value = null;

		if (val.getAllLocalizations().containsKey(loc)) {

			// loop through list and select the first one that has a start date
			// before right now.
			List<Localization> locs = val.getAllLocalizations().get(loc);
			Collections.sort(locs);
			Date now = new Date();
			for (int i = locs.size() - 1; i >= 0; i--) {
				Localization candidate = locs.get(i);
				if (candidate.getStartDate() == null
						|| candidate.getStartDate().before(now)) {
					value = candidate.getValue();
					break;
				}
			}
			// System.out.println("value found: " + value);
		} else {
			System.err.println("Value not found: " + identifier);
			Model.get();
			if (Model.conf.throwWhenValueNotFound()) {
				throw new ValueNotFoundException(identifier);
			}
			return identifier;
		}

		return value;
	}

	Version getPreviousVersion() {
		return getVersion(Direction.INCOMING);
	}

	Version getNextVersion() {
		return getVersion(Direction.OUTGOING);
	}

	private Version getVersion(Direction d) {
		Relationship r = node.getSingleRelationship(
				References.VERSION_TO_NEWER, d);
		if (r == null) {
			return null;
		}

		Version v = new Version(r.getOtherNode(node));
		return v;
	}

	/**
	 * @return if this {@link Version} is the current Production {@link Version}
	 *         , i.e. the most recently committed one.
	 */
	public boolean isProduction() {
		Node root = Model.db.getReferenceNode();

		int maxVersion = -1;
		for (Relationship r : root.getRelationships(References.ROOT_TO_VERSION)) {
			Node version = r.getEndNode();
			int nodeVersion = NodeUtil.getInt(version, Version.PROP_VERSION);
			if (nodeVersion != VERSION_PREVIEW && nodeVersion > maxVersion) {
				maxVersion = nodeVersion;
			}
		}

		return maxVersion == getVersionNumber();
	}

}
